
/**
 *	TrueMusic - simple java mp3 player
 *
 *	TrueMusic is free software; you can redistribute it
 *	and/or modify it under the terms of the GNU General
 *	Public License as published by the Free Software
 *	Foundation; either version 2 of the License or (at
 *	your option) any later version.
 *
 *	This program is distributed in the hope that it will be
 *	useful but WITHOUT ANY WARRANTY; without even the
 *	implied warranty of the MERCHANTABILITY or FITNESS FOR
 *	A PARTICULAR PURPOSE. See the GNU General Public
 *	License for more details.
 *
 *	Copyright (c)2005-2007 Ryan Macnak <rmacnak@gmail.com>
 */

package com.servequake.truemusic;

import java.io.*;
import javax.sound.sampled.*;

import com.jcraft.jorbis.*;
import com.jcraft.jogg.*;

/**
 * Ogg Vorbis audio implemented by Jorbis decoder.
 */
public class JorbisAudio implements Audio, Runnable
{
	private String title="";
	private String artist="";
	private String album="";
	private String track="";
	private String lyrics="";

	private String res;
	private PlaybackListener l;

	private int frames = 0;

	public void load(String res, PlaybackListener l) throws Exception
	{
		this.l = l;
		this.res = res;

		VorbisPrimer vp = new VorbisPrimer(res);

		this.title = vp.title;
		this.artist = vp.artist;
		this.album = vp.album;
		this.track = vp.track;
		this.frames = vp.frames;
		this.lyrics = vp.lyrics;
	}

	public String getResource()
	{
		return res;
	}

	boolean pause=true;
	int pos=0;

	/*org*/
	Thread player = null;
	InputStream bitStream = null;

	static final int BUFSIZE=4096*2;
	static int convsize=BUFSIZE*2;
	static byte[] convbuffer = new byte[convsize];


	SyncState oy;
	StreamState os;
	Page og;
	Packet op;
	Info vi;
	Comment vc;
	DspState vd;
	Block vb;
  
	byte[] buffer=null;
	int bytes=0;

	int format;
	int rate=0;
	int channels=0;
	int left_vol_scale=100;
	int right_vol_scale=100;
	SourceDataLine outputLine=null;

	int frameSizeInBytes;
	int bufferLengthInBytes;

	public synchronized void play()
	{
		if(player==null) (player = new Thread(this)).start();

		if(!pause) pos = 0;
		else
		{
			pause = false;
			notify();
			l.playbackStarted();
		}
	}

	public synchronized void pause()
	{
		if(player==null) (player = new Thread(this)).start();

		pause = !pause;
		if(!pause)
		{
			notify();
			l.playbackStarted();
		}
		else l.playbackStopped();
	}

	public synchronized void stop()
	{
		player = null;

		l.playbackStopped();
		l.frameCompleted(0);
	}


	public void run()	
	{
    int last_channels=-1;
    int last_rate=-1;

    boolean chained=false;

		oy=new SyncState();
		os=new StreamState();
		og=new Page();
		op=new Packet();
		vi=new Info();
		vc=new Comment();
		vd=new DspState();
		vb=new Block(vd);
  
		buffer=null;
		bytes=0;

		oy.init();

try
{
		bitStream = new FileInputStream(res);
}catch(Exception e){return;}

			top:
		while(true)
		{
				int eos = 0;

				int index = oy.buffer(BUFSIZE);
				buffer = oy.data;

				try{ bytes = bitStream.read(buffer,index, BUFSIZE); }
				catch(Exception e) { return; }

				oy.wrote(bytes);
	
				if(chained) chained=false;
				else
				{
					if(oy.pageout(og)!=1)
					{
						if(bytes<BUFSIZE) break;

						System.err.println("Input does not appear to be an Ogg bitstream.");
						return;
					}
				}

				os.init(og.serialno());
				os.reset();

				vi.init();
				vc.init();

				if(os.pagein(og)<0)
				{
					System.err.println("Error reading first page of ogg bitstream data.");
					return;
				}

				if(os.packetout(op)!=1)
				{
					System.err.println("Error reading initial header packet");
					break;
				}

				if(vi.synthesis_headerin(vc, op)<0)
				{
					System.err.println("This ogg bitstream does not contain vorbis audio data");
					return;
				}

				int i = 0;

				while(i<2)
				{
					while(i<2)
					{
						int result = oy.pageout(og);
						if(result==0) break; //need more data
						if(result==1)
						{
							os.pagein(og);
							while(i<2)
							{
								result = os.packetout(op);
								if(result==0) break;
								if(result==-1)
								{
									System.err.println("corrupt secondary header. exiting.");
									break top;
								}
								vi.synthesis_headerin(vc, op);
								i++;
							}
						}
					}

					index = oy.buffer(BUFSIZE);
					buffer = oy.data;
	
					try{ bytes = bitStream.read(buffer,index, BUFSIZE); }
					catch(Exception e)
					{
						e.printStackTrace();
						return;
					}
	
					if(bytes==0 && i<2)
					{
						System.err.println("End of file before finding all Vorbis headers!");
						return;
					}

					oy.wrote(bytes);
				} // end header loops?
			
			

			title = vc.query("title"); //appears to be case insensetive?
			artist = vc.query("artist");
			album = vc.query("album");
			track = vc.query("track");
			lyrics = vc.query("lyrics");
			if(title==null)title="";
			if(artist==null)artist="";
			if(album==null)album="";
			if(track==null)track="";
			if(lyrics==null)lyrics="";

						
				convsize=BUFSIZE/vi.channels;

				vd.synthesis_init(vi);
				vb.init(vd);

				double[][][] _pcm = new double[1][][];
				float[][][] _pcmf = new float[1][][];
				int[] _index = new int[vi.channels];

				getOutputLine(vi.channels, vi.rate);

			
			while(eos==0)
			{
				while(eos==0) 
				{
					int result = oy.pageout(og);
					if(result == 0) break; //need more data
					if(result==-1){} // missing or corrupt data at this page position, continuing...
					else
					{
						os.pagein(og);

						if(og.granulepos()==0)
						{
							chained=true;
							eos=1;
							break;
						}
	
						while(true)
						{
							result = os.packetout(op);
							if(result==0) break; //need more data
							if(result==-1)
							{//missing or corrupt data, already complained above
							}
							else
							{
								//we have a packet. Decode it
								int samples;
								if(vb.synthesis(op)==0){ //test for success!
						  			vd.synthesis_blockin(vb);
								}

								while( (samples=vd.synthesis_pcmout(_pcmf, _index))>0 )
								{
									synchronized(this)
									{
										if(player==null) break top;
										while(pause) try{ wait(); }catch(Exception e){}
					
										double[][] pcm = _pcm[0];
										float[][] pcmf = _pcmf[0];

										boolean clipflag=false;
										int bout = (samples<convsize?samples:convsize);

								//convert doubles to 16bit signed ints
for(i=0;i<vi.channels;i++){
		    int ptr2=i*2;
			int mono=_index[i];
		    for(int j=0;j<bout;j++){
		      int val=(int)(pcmf[i][mono+j]*32767.);
		      if(val>32767){
		        val=32767;
		        clipflag=true;
		      }
		      if(val<-32768){
		        val=-32768;
		        clipflag=true;
		      }
                      if(val<0) val=val|0x8000;
		      convbuffer[ptr2]=(byte)(val);
		      convbuffer[ptr2+1]=(byte)(val>>>8);
		      ptr2+=2*(vi.channels);
		    }
		  }

										vol.setValue(l.getVolume());

										outputLine.write(convbuffer, 0, 2*vi.channels*bout);
										vd.synthesis_read(bout);

										pos++;
										l.frameCompleted(pos);


									}//synch
								}//while samples
							}//else have a packet
							if(og.eos()!=0)eos=1;
						}
					}
				}//inner playloop


				if(eos==0)
				{
					index=oy.buffer(BUFSIZE);
					buffer=oy.data;
					try
					{
						bytes=bitStream.read(buffer,index,BUFSIZE);
					}
					catch(Exception e)
					{
						System.err.println(e);
            				return;
					}
          
					if(bytes==-1)
					{
            				//System.err.println("bytes==-1");
            				break;
	 				}
	  				oy.wrote(bytes);
	  				if(bytes==0)eos=1;
        			}


			}// outer play loop
      		os.clear();
      		vb.clear();
      		vd.clear();
      		vi.clear();
		}

		oy.clear();				
		

		if(!pause) l.playbackCompleted();

		player = null;
		pos = 0;
		pause = true;

    		try {
			if(bitStream!=null)bitStream.close();
		} 
		catch(Exception e) { }
	}

	FloatControl vol; 

	SourceDataLine getOutputLine(int channels, int rate)
	{
		try
		{
			AudioFormat af = new AudioFormat( (float)rate, 16, channels, true, false );
			DataLine.Info info = new DataLine.Info(SourceDataLine.class, af, AudioSystem.NOT_SPECIFIED);

			if( !AudioSystem.isLineSupported(info) ) throw new RuntimeException("AudioLine not supported!");

			outputLine = (SourceDataLine) AudioSystem.getLine(info);
			outputLine.open(af);
			outputLine.start(); //the magic line: this was the one missing that i couldn't get it to start :(

			vol = (FloatControl)outputLine.getControl(FloatControl.Type.MASTER_GAIN);

			frameSizeInBytes = af.getFrameSize();
			int bufferLengthInFrames = outputLine.getBufferSize()/frameSizeInBytes/2;
			bufferLengthInBytes = bufferLengthInFrames * frameSizeInBytes;

			this.rate = rate;
			this.channels = channels;
			
			return outputLine;
		}
		catch(LineUnavailableException e)
		{
			e.printStackTrace();
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
		return null;
	}


	public boolean isPlaying()
	{
		return !pause;
	}

	public int getFrames() {return frames;}
	public void seek(int frame){}

	

	public String getTitle()
	{
		if(title.equals("")) return new File(res).getName();
		return title;
	}

	public String getArtist() {return artist;}
	public String getAlbum() {return album;}
	public String getTrack() {return track;}

	public String getGenre()
	{
		return "";
	}

	public String getLyrics() {return lyrics;}

	public String toString()
	{
		if(title.equals("")) return res;
		return "\""+title +"\" by "+artist;
	}

	public String[] getExtensions()
	{
		return new String[]{".ogg"};
	}

	public String formatName()
	{
		return "Ogg Vorbis";
	}
}
